﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.Reflection;
using System.Linq;

using BizElements.Core;

namespace BizElements.BusinessLayer
{
    /// <summary>Provides common OR/M and data access operations.</summary>
    public static class OrmUtil
    {
        #region Private helper methods.

        private static IEntityDAO GetDao(IEntity entity, IConnectionProvider conn)
        {
            return EntityDaoFactory.GetEntityDAO(entity, conn);
        }

        private static IEntityDAO GetDao(IDbTable table, IConnectionProvider conn)
        {
            return EntityDaoFactory.GetEntityDAO(table.NewEntity(), conn);
        }

        private static SearchCondition CreatePkFilter(IEntity entity)
        {
            SearchCondition filter = new SearchCondition();
            foreach (IDbColumn field in entity.Table.Columns)
            {
                if (field.IsPrimaryKeyPart)
                    filter.And(field, entity.GetField(field));
            }

            return filter;
        }

        private static IEntity NewEntity(IDbTable table, int id)
        {
            DbColumnCollection pk = table.PrimaryKey;
            if (pk.Count != 1 || pk[0].DataType != typeof(int))
                throw new InvalidOperationException("PK must be constructed of exactly one 32bit integer field.");

            IEntity entity = table.NewEntity();
            entity.SetField(pk[0], id);
            return entity;
        }

        #endregion

        #region GetChangedFields.

        /// <summary>
        /// Compares two entities from the same table and returns the fields which are different.
        /// </summary>
        /// <param name="oldVersion">Old version.</param>
        /// <param name="newVersion">New version.</param>
        /// <returns>A collection of columns that have changed.</returns>
        public static IEnumerable<IDbColumn> GetChangedFields(IEntity oldVersion, IEntity newVersion)
        {
            if (oldVersion.Table.TableName != newVersion.Table.TableName)
                throw new InvalidOperationException("Entities must belong to the same table.");

            List<IDbColumn> changedFields = new List<IDbColumn>();
            object[] oldFieldValues = oldVersion.ToObjectArray();
            object[] newFieldValues = newVersion.ToObjectArray();
            for (int idxField = 0; idxField < oldFieldValues.Length; idxField++)
            {
                string oldValue = Convert.ToString(oldFieldValues[idxField], CultureInfo.InvariantCulture);
                string newValue = Convert.ToString(newFieldValues[idxField], CultureInfo.InvariantCulture);
                if (oldValue != newValue)
                    changedFields.Add(oldVersion.Table.Columns[idxField]);
            }

            return changedFields;
        }

        #endregion

        #region SortDataTable.

        /// <summary>
        /// Sorts DataTable.
        /// </summary>
        /// <param name="data">Data to sort.</param>
        /// <param name="column">Column name by which the data will be sorted.</param>
        /// <returns>Sorted data.</returns>
        public static DataTable SortDataTable(DataTable data, string column)
        {
            return AdoNetUtil.SortDataTable(data, column);
        }

        /// <summary>
        /// Sorts DataTable.
        /// </summary>
        /// <param name="data">Data to sort.</param>
        /// <param name="column">Column name by which the data will be sorted.</param>
        /// <param name="ascending">Specifies whether the values in the specified column should be sorted in ascendin order, from lowest value to highest value.</param>
        /// <returns>Sorted data.</returns>
        private static DataTable SortDataTable(DataTable data, string column, bool ascending)
        {
            return AdoNetUtil.SortDataTable(data, column, ascending);
        }

        #endregion

        #region Connections and transactions.

        /// <summary>
        /// Ensures the connection provider has a pending transaction.
        /// </summary>
        /// <param name="conn">Connection-transaction context to manage.</param>
        /// <returns><b>true</b> if local transaction was started; <b>false</b> if transaction was already started.</returns>
        public static bool RequestTransaction(IConnectionProvider conn)
        {
            bool isLocalTran = !conn.IsTransactionPending;
            if (isLocalTran)
                conn.BeginTransaction();

            return isLocalTran;
        }

        /// <summary>
        /// Commits transaction if it is local, ie. started in the current method.
        /// </summary>
        /// <param name="conn">Connection-transaction context to manage.</param>
        /// <param name="isLocalTran">Whether to commit transaction.</param>
        public static void CommitTransactionIfLocal(IConnectionProvider conn, bool isLocalTran)
        {
            if (isLocalTran)
                conn.CommitTransaction();
        }

        /// <summary>
        /// Closes connection if it is local, ie. openned in the current method.
        /// </summary>
        /// <param name="conn">Connection-transaction context to manage.</param>
        /// <param name="isLocalConn">Whether to close connection.</param>
        public static void CloseConnectionIfLocal(IConnectionProvider conn, bool isLocalConn)
        {
            if (isLocalConn)
                conn.CloseConnection();
        }

        #endregion

        #region UpdateEntitiesWithCompositePkRemovedFromCollection.

        /// <summary>Updates status of entities which were remove from a colleciton. <b>Requires direct DB access. Doesn't support RPC</b>.</summary>
        /// <param name="conn">Connection-transaction context to use.</param>
        /// <param name="modifiedCollection">Collection from which entities have been removed.</param>
        /// <param name="oldSerializedPrimaryKeys">Array of serialized primary keys before the collection was modified.</param>
        /// <param name="statusColumn">Status field to update.</param>
        /// <param name="deactivatedValue">Value to set to status field.</param>
        /// <remarks>This method is commonly used for child entities with composite keys.</remarks>
        public static void UpdateEntitiesWithCompositePkRemovedFromCollection(IConnectionProvider conn, IEntityCollection modifiedCollection, string[] oldSerializedPrimaryKeys, IDbColumn statusColumn, object deactivatedValue)
        {
            UpdateList setStatusField = new UpdateList(statusColumn, deactivatedValue);
            UpdateEntitiesWithCompositePkRemovedFromCollection(conn, modifiedCollection, oldSerializedPrimaryKeys, setStatusField);
        }

        /// <summary>Updates status of entities which were remove from a colleciton. <b>Requires direct DB access. Doesn't support RPC</b>.</summary>
        /// <param name="conn">Connection-transaction context to use.</param>
        /// <param name="modifiedCollection">Collection from which entities have been removed.</param>
        /// <param name="oldSerializedPrimaryKeys">Array of serialized primary keys before the collection was modified.</param>
        /// <param name="setExpressions">A list of field-value pairs to update.</param>
        /// <remarks>This method is commonly used for child entities with composite keys.</remarks>
        public static void UpdateEntitiesWithCompositePkRemovedFromCollection(IConnectionProvider conn, IEntityCollection modifiedCollection, string[] oldSerializedPrimaryKeys, UpdateList setExpressions)
        {
            var removedChildren = CreateEntitiesRemovedFromCollection(modifiedCollection, oldSerializedPrimaryKeys);
            IEntityDAO dao = DAL.GetDao(removedChildren.IDbTable, conn);
            foreach (IEntity removedEntity in removedChildren)
                dao.Update(setExpressions, CreatePrimaryKeyFilter(removedEntity));
        }

        private static EntityCollection CreateEntitiesRemovedFromCollection(IEntityCollection modifiedCollection, string[] oldSerializedPrimaryKeys)
        {
            string[] currentChildrenPKs = PrimaryKeySerializer.Serialize(modifiedCollection.IDbTable, modifiedCollection);
            var removedChildrenPKs = ArrayUtil.GetDifference<List<string>, string>(oldSerializedPrimaryKeys, currentChildrenPKs);
            EntityCollection removedChildren = new EntityCollection(modifiedCollection.IDbTable.Clone(null));
            PrimaryKeySerializer.Deserialize(removedChildren.IDbTable, removedChildrenPKs, removedChildren);

            return removedChildren;
        }

        private static EntityCollection CreateEntitiesRemovedFromCollection<TEntity>(IEnumerable<TEntity> modifiedCollection, string[] oldSerializedPrimaryKeys)
            where TEntity : IEntity, new()
        {
            string[] currentChildrenPKs = PrimaryKeySerializer.Serialize<TEntity>(modifiedCollection);
            var removedChildrenPKs = ArrayUtil.GetDifference<List<string>, string>(oldSerializedPrimaryKeys, currentChildrenPKs);
            EntityCollection removedChildren = new EntityCollection(new TEntity().Table);
            PrimaryKeySerializer.Deserialize(removedChildren.IDbTable, removedChildrenPKs, removedChildren);

            return removedChildren;
        }

        private static SearchCondition CreatePrimaryKeyFilter(IEntity entity)
        {
            SearchCondition entityCriteria = new SearchCondition();
            foreach (IDbColumn pkPart in entity.Table.PrimaryKey)
            {
                object pkValue = entity.GetField(pkPart);
                if (pkValue == null)
                    throw new ArgumentException("Primary key is not set in the provided entity.");

                entityCriteria.And(pkPart, pkValue);
            }

            return entityCriteria;
        }

        #endregion

        #region DeleteEntitiesWithCompositePkRemovedFromCollection.

        /// <summary>Deletes entities which were removed from a collection. <b>Requires direct DB access. Doesn't support RPC</b>.</summary>
        /// <param name="conn">Connection-transaction context to use.</param>
        /// <param name="modifiedCollection">Collection from which entities have been removed.</param>
        /// <param name="oldSerializedPrimaryKeys">Array of serialized primary keys before the collection was modified.</param>
        /// <remarks>This method is commonly used for child entities with composite keys.</remarks>
        public static void DeleteEntitiesWithCompositePkRemovedFromCollection(IConnectionProvider conn, IEntityCollection modifiedCollection, string[] oldSerializedPrimaryKeys)
        {
            var removedChildren = CreateEntitiesRemovedFromCollection(modifiedCollection, oldSerializedPrimaryKeys);
            foreach (IEntity entity in removedChildren)
                entity.EntityState = EntityState.PendingDeletion;

            DAL.GetDao(removedChildren.IDbTable, conn).SaveCollection(removedChildren);
        }

        /// <summary>Deletes entities which were removed from a collection. <b>Requires direct DB access. Doesn't support RPC</b>.</summary>
        /// <param name="conn">Connection-transaction context to use.</param>
        /// <param name="modifiedCollection">Collection from which entities have been removed.</param>
        /// <param name="oldSerializedPrimaryKeys">Array of serialized primary keys before the collection was modified.</param>
        /// <remarks>This method is commonly used for child entities with composite keys.</remarks>
        public static void DeleteEntitiesWithCompositePkRemovedFromCollection<TEntity>(IConnectionProvider conn, IEnumerable<TEntity> modifiedCollection, string[] oldSerializedPrimaryKeys)
            where TEntity : IEntity, new()
        {
            var removedChildren = CreateEntitiesRemovedFromCollection(modifiedCollection, oldSerializedPrimaryKeys);
            foreach (IEntity entity in removedChildren)
                entity.EntityState = EntityState.PendingDeletion;

            DAL.GetDao(removedChildren.IDbTable, conn).SaveCollection(removedChildren);
        }

        #endregion
                
        /// <summary>Creates a update expressions that sets all child foreign key fields to null.</summary>
        /// <param name="relation">Relation.</param>
        /// <returns>SET clause.</returns>
        public static UpdateList CreateSetForeignKeyFieldsToNullClause(DbRelation relation)
        {
            UpdateList setFkFields = new UpdateList();
            foreach (IDbColumn fkField in relation.ChildForeignKey)
                setFkFields.Add(fkField, null);

            return setFkFields;
        }

        #region EnsureParents.

        /// <summary>
        /// Fetches parents whose FK is defined but parent object is not set. <b>RPC enabled.</b>
        /// </summary>
        /// <param name="child">Child entity which is to be checked and updated.</param>
        /// <param name="parentsToFetch">Relations (foreign keys) to parents which are to be fetched. Child table of the given FK must be the current entity's table; otherwise an exception is generated.</param>
        public static void EnsureParents(IEntity child, params DbRelation[] parentsToFetch)
        {
            List<DbRelation> fksToMissingParents = new List<DbRelation>();
            foreach (DbRelation fk in parentsToFetch)
            {
                if (child.IsForeignKeySet(fk))
                {
                    if (child.GetParent(fk) == null)
                        fksToMissingParents.Add(fk);
                }
            }

            if (fksToMissingParents.Count == 0)
                return;

            IEntity[] parents = RpcHelper.InvokeStaticMethod<IEntity[], IEntity, DbRelation[]>(DbFetchParents, child, fksToMissingParents.ToArray());
            for (int idxParent = 0; idxParent < parents.Length; idxParent++)
            {
                DbRelation fk = fksToMissingParents[idxParent];
                if (child.GetParent(fk) == null)
                    child.SetParent(fk, parents[idxParent]);
            }
        }

        private static IEntity[] DbFetchParents(IEntity child, DbRelation[] parentsToFetch)
        {
            IEntityDAO dao = GetDao(child, child.Table.Catalog.CreateConnectionProvider());
            return dao.FetchParents(parentsToFetch);
        }

        #endregion

        #region EnsureParentsForCollection.

        /// <summary>
        /// Fetches parents whose FK is defined but parent object is not set. <b>RPC enabled.</b>
        /// </summary>
        /// <param name="children">Child entities which are to be checked and updated.</param>
        /// <param name="parentsToFetch">Relations (foreign keys) to parents which are to be fetched. Child table of the given FK must be the current entities' table; otherwise an exception is generated.</param>
        public static void EnsureParentsForCollection(IList children, params DbRelation[] parentsToFetch)
        {
            // First, we reduce the list. This enhances performance if remoting is required.
            int[] idxOfKidsWithoutParents = ArrayUtil.FindAllIndexes<IEntity>(children, (kid) => !HasAllDefinedParents(kid, parentsToFetch));
            if (idxOfKidsWithoutParents.Length == 0)
                return;
            
            foreach (int i in idxOfKidsWithoutParents)
                EnsureParents((IEntity)children[i], parentsToFetch);
        }

        private static bool HasAllDefinedParents(IEntity child, params DbRelation[] requiredParentsIfFkIsSet)
        {
            foreach (DbRelation fk in requiredParentsIfFkIsSet)
            {
                bool hasFkButParentIsNotSet = child.IsForeignKeySet(fk) && (child.GetParent(fk) == null);
                if (hasFkButParentIsNotSet)
                    return false;
            }

            return true;
        }

        #endregion

        #region EnsureGrandParents.

        /// <summary>
        /// Fetches parent with grand parents whose FK is defined but parent object is not set. <b>RPC enabled.</b>
        /// </summary>
        /// <param name="child">Child entity which is to be checked and updated.</param>
        /// <param name="parentToFetch">Relation (foreign key) to parent which is to be fetched. Child table of the given FK must be the current entity's table; otherwise an exception is generated.</param>
        /// <param name="grandParentsToFetch">Relations (foreign keys) between parent and grand parents which are to be fetched.</param>
        public static void EnsureGrandParents(IEntity child, DbRelation parentToFetch, params DbRelation[] grandParentsToFetch)
        {
            EnsureParents(child, parentToFetch);
            IEntity parent = child.GetParent(parentToFetch);
            if (parent != null)
            {
                EnsureParents(parent, grandParentsToFetch);
            }
        }

        /// <summary>
        /// Fetches parents with grand parents whose FK is defined but parent object is not set. <b>RPC enabled.</b>
        /// </summary>
        /// <param name="child">Child entity which is to be checked and updated.</param>
        /// <param name="parentsToFetch">Relations (foreign keys) to parents which are to be fetched. Child table of the given FK must be the current entity's table; otherwise an exception is generated.</param>
        /// <param name="grandParentsToFetch">Relations (foreign keys) between parents and grand parents which are to be fetched.</param>
        public static void EnsureGrandParents(IEntity child, DbRelation[] parentsToFetch, DbRelation[] grandParentsToFetch)
        {
            EnsureParents(child, parentsToFetch);
            foreach (DbRelation fk in parentsToFetch)
            {
                IEntity parent = child.GetParent(fk);
                if (parent != null)
                {
                    List<DbRelation> grandParentsForCurrParent = ArrayUtil.FindAll<List<DbRelation>, DbRelation>(grandParentsToFetch,
                        (rel) => (rel.Child.HasEqualAliasAndNameAs(parent.Table)));

                    EnsureParents(parent, grandParentsForCurrParent.ToArray());
                }
            }
        }

        #endregion

        #region FetchEntity.

        /// <summary>
        /// Fetches an entity from data source. <b>RPC enabled.</b>
        /// </summary>
        /// <param name="entityWithPK">Entity whose primary key fields have been set to a valid value. This object is not modified.</param>
        /// <param name="parentsToFetch">Relations (foreign keys) to parents which are going to be fetched.</param>
        /// <returns>Requested entity.</returns>
        /// <exception cref="InvalidOperationException">Generated if the class of requested object is not marked as serializable
        /// or if the object with the provided PK doesn't exist in the data source.</exception>
        public static IEntity FetchEntity(IEntity entityWithPK, params DbRelation[] parentsToFetch)
        {
            return RpcHelper.InvokeStaticMethod<IEntity, IEntity, DbRelation[]>(DbFetchEntity, entityWithPK, parentsToFetch);
        }

        private static IEntity DbFetchEntity(IEntity entityWithPK, params DbRelation[] parentsToFetch)
        {
            if (!entityWithPK.GetType().IsSerializable)
                throw new InvalidOperationException(entityWithPK.GetType().FullName + " is not marked as Serializable.");

            IEntityDAO dao = GetDao(entityWithPK, null);
            bool exists = dao.SelectOne(parentsToFetch);
            if (!exists)
                throw new InvalidOperationException("Table '" + entityWithPK.Table.TableName + "' doesn't contain a record with the requested PK: " + ArrayUtil.Concat(entityWithPK.GetPrimaryKeyValue(), ","));

            return entityWithPK;
        }

        /// <summary>
        /// Fethes an entity from data source.
        /// </summary>
        /// <typeparam name="TEntity">BizElements generated entity class.</typeparam>
        /// <param name="id">ID of the object to fetch.</param>
        /// <param name="parentsToFetch">Relations (foreign keys) to parents which are going to be fetched.</param>
        /// <returns>Requested entity.</returns>
        /// <exception cref="InvalidOperationException">Generated if the class of requested object is not marked as serializable
        /// or if the object with the provided PK doesn't exist in the data source.</exception>
        public static TEntity FetchEntity<TEntity>(int id, params DbRelation[] parentsToFetch)
            where TEntity : IEntity, new()
        {
            IEntity entityWithPK = new TEntity();
            DbColumnCollection pk = entityWithPK.Table.PrimaryKey;
            if (pk.Count != 1 || !TypeUtil.IsInteger(pk[0].DataType))
                throw new InvalidOperationException("PK must be constructed of exactly one integer field.");

            entityWithPK.SetField(pk[0], id);
            return (TEntity)FetchEntity(entityWithPK, parentsToFetch);
        }

        /// <summary>
        /// Fethes an entity from data source.
        /// </summary>
        /// <typeparam name="TEntity">BizElements generated entity class.</typeparam>
        /// <param name="id">ID of the object to fetch.</param>
        /// <param name="parentsToFetch">Relations (foreign keys) to parents which are going to be fetched.</param>
        /// <returns>Requested entity.</returns>
        /// <exception cref="InvalidOperationException">Generated if the class of requested object is not marked as serializable
        /// or if the object with the provided PK doesn't exist in the data source.</exception>
        public static TEntity FetchEntity<TEntity>(long id, params DbRelation[] parentsToFetch)
            where TEntity : IEntity, new()
        {
            IEntity entityWithPK = new TEntity();
            DbColumnCollection pk = entityWithPK.Table.PrimaryKey;
            if (pk.Count != 1 || !TypeUtil.IsInteger(pk[0].DataType))
                throw new InvalidOperationException("PK must be constructed of exactly one integer field.");

            entityWithPK.SetField(pk[0], id);
            return (TEntity)FetchEntity(entityWithPK, parentsToFetch);
        }

        /// <summary>
        /// Fethes an entity from data source.
        /// </summary>
        /// <typeparam name="TEntity">BizElements generated entity class.</typeparam>
        /// <param name="id">ID of the object to fetch.</param>
        /// <param name="parentsToFetch">Relations (foreign keys) to parents which are going to be fetched.</param>
        /// <returns>Requested entity.</returns>
        /// <exception cref="InvalidOperationException">Generated if the class of requested object is not marked as serializable
        /// or if the object with the provided PK doesn't exist in the data source.</exception>
        public static TEntity FetchEntity<TEntity>(string id, params DbRelation[] parentsToFetch)
            where TEntity : IEntity, new()
        {
            IEntity entityWithPK = new TEntity();
            DbColumnCollection pk = entityWithPK.Table.PrimaryKey;
            if (pk.Count != 1 || !TypeUtil.IsText(pk[0].DataType))
                throw new InvalidOperationException("PK must be constructed of exactly one textual field.");

            entityWithPK.SetField(pk[0], id);
            return (TEntity)FetchEntity(entityWithPK, parentsToFetch);
        }

        /// <summary>Fethes an entity from data source.</summary>
        /// <typeparam name="TEntity">BizElements generated entity class.</typeparam>
        /// <param name="pkValues">Composte PK values in order as defined in acompanying metadata/table class.</param>
        /// <param name="parentsToFetch">Relations (foreign keys) to parents which are going to be fetched.</param>
        /// <returns>Requested entity.</returns>
        /// <exception cref="InvalidOperationException">Generated if the class of requested object is not marked as serializable
        /// or if the object with the provided PK doesn't exist in the data source.</exception>
        public static TEntity FetchEntity<TEntity>(object[] pkValues, params DbRelation[] parentsToFetch)
            where TEntity : IEntity, new()
        {
            IEntity entityWithPK = new TEntity();
            DbColumnCollection pkFields = entityWithPK.Table.PrimaryKey;
            for (int i = 0; i < pkFields.Count; i++ )
                entityWithPK.SetField(pkFields[i], pkValues[i]);

            return (TEntity)FetchEntity(entityWithPK, parentsToFetch);
        }

        #endregion

        #region UpdateLinksToEntitiesRemovedFromCollection.

        /// <summary>
        /// Updates status of link records in a junction table which point to entities removed from a collection. <b>Requires direct DB access. Doesn't support RPC</b>.
        /// </summary>
        /// <param name="conn">Connection-transaction context to use.</param>
        /// <param name="parentFkField">Field which connects link records to their parent.</param>
        /// <param name="parentFkValue">ID of the parent entity.</param>
        /// <param name="modifiedCollection">Collection from which entities have been removed.</param>
        /// <param name="oldIDs">Array of IDs before the collection was modified. May be <b>null</b> or empty.</param>
        /// <param name="linkToEntities">A relation connectis a junction table to table which contains contains the entities in the <b>modifiedCollection</b> argument.</param>
        /// <param name="statusColumn">Status field to update.</param>
        /// <param name="deactivatedValue">Value to set to status field.</param>
        public static void UpdateLinksToEntitiesRemovedFromCollection(IConnectionProvider conn, IDbColumn parentFkField, object parentFkValue, IEntityCollection modifiedCollection, int[] oldIDs, DbRelation linkToEntities, IDbColumn statusColumn, object deactivatedValue)
        {
            UpdateLinksToEntitiesRemovedFromCollection(conn, new SearchCondition(parentFkField, parentFkValue), modifiedCollection, oldIDs, linkToEntities, new UpdateList(statusColumn, deactivatedValue));
        }

        /// <summary>
        /// Updates status of link records in a junction table which point to entities removed from a collection. <b>Requires direct DB access. Doesn't support RPC</b>.
        /// </summary>
        /// <param name="conn">Connection-transaction context to use.</param>
        /// <param name="parentFk">Criteria which connects link records to their parent, ie. foreign key fields with parent ID.</param>
        /// <param name="modifiedCollection">Collection from which entities have been removed.</param>
        /// <param name="oldIDs">Array of IDs before the collection was modified. May be <b>null</b> or empty.</param>
        /// <param name="linkToEntities">A relation connectis a junction table to table which contains contains the entities in the <b>modifiedCollection</b> argument.</param>
        /// <param name="setExpressions">A list of field-value pairs to update.</param>
        public static void UpdateLinksToEntitiesRemovedFromCollection(IConnectionProvider conn, SearchCondition parentFk, IEntityCollection modifiedCollection, int[] oldIDs, DbRelation linkToEntities, UpdateList setExpressions)
        {
            if (linkToEntities.Parent.TableName != modifiedCollection.IDbTable.TableName)
                throw new InvalidOperationException("Relation specified in the linkToEntities argument does not point to table which contains the entities in the modifiedCollection argument.");
            if (linkToEntities.ChildForeignKey.Length != 1)
                throw new ArgumentException("Primary and foreign keys must be constructed of exactly one integer column.");
            IDbColumn fk = linkToEntities.ChildForeignKey[0];
            if (fk.DataType != typeof(int))
                throw new ArgumentException("Primary and foreign keys must be constructed of exactly one integer column.");
            if (ArrayUtil.IsNullOrEmpty(oldIDs))
                return;

            int[] remainingIDs = ArrayUtil.ToArray<int>(modifiedCollection, linkToEntities.ParentPrimaryKey[0].PropertyName);
            int[] removedIDs = ArrayUtil.GetDifference(oldIDs, remainingIDs);
            if (removedIDs.Length == 0)
                return;

            IDbTable linkTable = linkToEntities.Child;
            UpdateStatement update = new UpdateStatement(linkTable);
            update.UpdateList = setExpressions;
            update.Where.Add(parentFk);
            update.Where.And(PredicateFactory.In(fk, removedIDs));
            update.Execute(conn);
        }

        /// <summary>
        /// Updates status of link records in a junction table which point to entities removed from a collection. <b>Requires direct DB access. Doesn't support RPC</b>.
        /// </summary>
        /// <typeparam name="T">Type of values used in primary key field.</typeparam>
        /// <param name="conn">Connection-transaction context to use.</param>
        /// <param name="parentFk">Criteria which connects link records to their parent, ie. foreign key fields with parent ID.</param>
        /// <param name="modifiedCollection">Collection from which entities have been removed.</param>
        /// <param name="oldIDs">Array of IDs before the collection was modified. May be <b>null</b> or empty.</param>
        /// <param name="linkToEntities">A relation connectis a junction table to table which contains contains the entities in the <b>modifiedCollection</b> argument.</param>
        /// <param name="setExpressions">A list of field-value pairs to update.</param>
        public static void UpdateLinksToEntitiesRemovedFromCollection<T>(IConnectionProvider conn, SearchCondition parentFk, IEntityCollection modifiedCollection, T[] oldIDs, DbRelation linkToEntities, UpdateList setExpressions)
        {
            if (linkToEntities.Parent.TableName != modifiedCollection.IDbTable.TableName)
                throw new InvalidOperationException("Relation specified in the linkToEntities argument does not point to table which contains the entities in the modifiedCollection argument.");
            if (linkToEntities.ChildForeignKey.Length != 1)
                throw new ArgumentException("Primary and foreign keys must be constructed of exactly one column.");
            
            IDbColumn fk = linkToEntities.ChildForeignKey[0];
            if (ArrayUtil.IsNullOrEmpty(oldIDs))
                return;

            T[] remainingIDs = ArrayUtil.ToArray<T>(modifiedCollection, linkToEntities.ParentPrimaryKey[0].PropertyName);
            var removedIDs = ArrayUtil.GetDifference<List<T>, T>(oldIDs, remainingIDs);
            if (ArrayUtil.IsNullOrEmpty(removedIDs))
                return;

            IDbTable linkTable = linkToEntities.Child;
            UpdateStatement update = new UpdateStatement(linkTable);
            update.UpdateList = setExpressions;
            update.Where.Add(parentFk);
            update.Where.And(PredicateFactory.In(fk, removedIDs.ToArray()));
            update.Execute(conn);
        }

        #endregion

        #region InsertLinksToNewEntitiesInCollection.

        /// <summary>
        /// Inserts links (link table entities) which point to new parent entities in a collection. <b>Requires direct DB access. Doesn't support RPC</b>.
        /// </summary>
        /// <typeparam name="TLink">Type of entities which represent records in the junction/link table.</typeparam>
        /// <typeparam name="TParent">Type of parent entities in the given collection.</typeparam>
        /// <param name="conn">Connection-transaction context to use.</param>
        /// <param name="modifiedCollection">Collection to which entities have been added and persisted.</param>
        /// <param name="oldIDs">Array of IDs before the collection was modified. May be <b>null</b> or empty.</param>
        /// <param name="linkBuilder">A delegate which initializes a new link entity which points to new parent entity. Should be an instance method which knows how to initialize all other required fields.</param>
        public static void InsertLinksToNewEntitiesInCollection<TLink, TParent>(IConnectionProvider conn, IEntityCollection modifiedCollection, int[] oldIDs, Delegates.Function<TLink, TParent> linkBuilder)
            where TLink : IEntity, new()
            where TParent : IEntity, new()
        {
            if (modifiedCollection.IDbTable.PrimaryKey.Count != 1) throw new ArgumentException("Primary key must be constructed of exactly one integer column.");
            IDbColumn pk = modifiedCollection.IDbTable.PrimaryKey[0];
            if (pk.DataType != typeof(int)) throw new ArgumentException("Primary key must be constructed of exactly one integer column.");

            if (oldIDs == null)
                oldIDs = new int[0];

            Predicate<TParent> pkNotInOldIDs = (ent) => !ArrayUtil.Contains(oldIDs, Convert.ToInt32(ent.GetPrimaryKeyValue()[0]));
            List<TParent> newParents = ArrayUtil.FindAll<List<TParent>, TParent>(modifiedCollection, pkNotInOldIDs);
            if (newParents.Count == 0)
                return;

            IDbTable linkTable = new TLink().Table;
            EntityCollection linksToNewParents = new EntityCollection(linkTable);
            foreach (TParent parent in newParents)
                linksToNewParents.Add(linkBuilder(parent));
            IEntityDAO dao = GetDao(linksToNewParents.IDbTable, conn);
            dao.SaveCollection(linksToNewParents);
        }

        #endregion
      
        #region IsSubNode.

        /// <summary>
        /// Checks whether one record in the hiearchical table is below the other one.
        /// </summary>
        /// <param name="recursiveRelation">Recursive relation which defines a hierarchy.</param>
        /// <param name="parentNode">A node higher in the tree.</param>
        /// <param name="subNode">A node which references the parent node either directly or through other records which belong to the same (sub)tree.</param>
        /// <returns><b>true</b> if the sub-node belongs to that same tree as the parent node; otherwise <b>false</b>.</returns>
        /// <remarks><para>The method also return <b>true</b> if both specified nodes represent the same record, i.e. have equal primary keys.</para>
        /// <para>The term sub-node is used for a node which references the parent node either directly or through other 
        /// records which belong to the same (sub)tree.</para>
        /// <para>Note, this method only tests data which are already persisted in the data source.
        /// It doesn't analyze unpersisted, i.e. new or changed, data set to entities' properties.</para></remarks>
        public static bool IsSubNode(DbRelation recursiveRelation, IEntity parentNode, IEntity subNode)
        {
            return RpcHelper.InvokeStaticMethod<bool, DbRelation, IEntity, IEntity>(DbIsSubNode, recursiveRelation, parentNode, subNode);
        }

        private static bool DbIsSubNode(DbRelation recursiveRelation, IEntity parentNode, IEntity subNode)
        {
            EnsureAllObjectsReferToSameTable(recursiveRelation, parentNode, subNode);
            IEntityDAO daoParent = GetDao(parentNode, null);
            return daoParent.IsSubNode(recursiveRelation, subNode);
        }        

        private static void EnsureAllObjectsReferToSameTable(DbRelation recursiveRelation, IEntity node1, IEntity node2)
        {
            string tbl = recursiveRelation.Child.TableName;
            bool onlyOneTable = (tbl == recursiveRelation.Parent.TableName) && (tbl == node1.Table.TableName) && (tbl == node2.Table.TableName);
            if (!onlyOneTable)
                throw new InvalidOperationException("All objects must refer to the same database table/view.");
        }

        /// <summary>
        /// Checks whether one record in the hiearchical table is below the other one.
        /// </summary>
        /// <param name="recursiveRelation">Recursive relation which defines a hierarchy.</param>
        /// <param name="parentNodeId">A node higher in the tree.</param>
        /// <param name="subNodeId">A node which references the parent node either directly or through other records which belong to the same (sub)tree.</param>
        /// <returns><b>true</b> if the sub-node belongs to that same tree as the parent node; otherwise <b>false</b>.</returns>
        /// <remarks><para>The method also return <b>true</b> if both specified nodes represent the same record, i.e. have equal primary keys.</para>
        /// <para>The term sub-node is used for a node which references the parent node either directly or through other 
        /// records which belong to the same (sub)tree.</para>
        /// <para>Note, this method only tests data which are already persisted in the data source.
        /// It doesn't analyze unpersisted, i.e. new or changed, data set to entities' properties.</para></remarks>
        public static bool IsSubNode(DbRelation recursiveRelation, int parentNodeId, int subNodeId)
        {
            return RpcHelper.InvokeStaticMethod<bool, DbRelation, int, int>(DbIsSubNode, recursiveRelation, parentNodeId, subNodeId);
        }

        private static bool DbIsSubNode(DbRelation recursiveRelation, int parentNodeId, int subNodeId)
        {
            EnsureRecursiveRelation(recursiveRelation);
            IDbTable table = recursiveRelation.Parent;
            IEntity parentNode = NewEntity(table, parentNodeId);
            IEntity subNode = NewEntity(table, subNodeId);

            IEntityDAO daoParent = GetDao(parentNode, null);
            return daoParent.IsSubNode(recursiveRelation, subNode);
        }

        private static void EnsureRecursiveRelation(DbRelation recursiveRelation)
        {
            bool onlyOneTable = (recursiveRelation.Child.TableName == recursiveRelation.Parent.TableName);
            if (!onlyOneTable)
                throw new InvalidOperationException("The provided relation does not specify a recursive relation.");
        }

        #endregion

        #region ValidateChildren.

        /// <summary>Converts entities to ISelfValidatingObject instances and validates them.</summary>
        /// <param name="children">A collection of objects which implement ISelfValidatingObject interfaces or can be converted to it by using the provided converter delegate.</param>
        /// <param name="childrenPropertyName">Optional name of the parent object's property that contains child collection.</param>
        /// <param name="convertToBizObjMethod">A method which ensures that children collections contains ISelfValidatingObject instances.</param>
        /// <param name="level">Validation level.</param>
        /// <param name="messageSourceCulture">Current culture.</param>
        /// <returns>A collection of errors.</returns>
        public static BusinessRuleCollection ValidateChildren(IEntityCollection children, string childrenPropertyName, Delegates.Procedure<IEntityCollection, CultureInfo> convertToBizObjMethod, ValidationLevel level, CultureInfo messageSourceCulture)
        {
            BusinessRuleCollection rules = new BusinessRuleCollection();
            convertToBizObjMethod(children, messageSourceCulture);
            foreach (ISelfValidatingObject bizobj in children)
                rules.Add(bizobj.Validate(level));

            if (!string.IsNullOrEmpty(childrenPropertyName))
            {
                foreach (BusinessRule rule in rules)
                    rule.AddField(childrenPropertyName);
            }

            return rules.GetBrokenRules();
        }

        #endregion

        #region CheckFieldConstraints.

        /// <summary>
        /// Creates a converter that transforms BizElements errors to BusinessRule objects.
        /// </summary>
        /// <param name="bizobj">Business object the converter is created for.</param>
        /// <param name="dbEntity">BizElements entity used by the given business object. Sometimes this will the same object.</param>
        /// <param name="captions">Objects that provides field names. Sometimes this will be the same object.</param>
        /// <returns>Business rule with a status that depends on BizElements error code.</returns>
        public static FieldErrorsToRulesConverter CreateFieldErrorsToRulesConverter(ISelfValidatingObject bizobj, IEntity dbEntity, ICaptionProvider captions)
        {
            FieldErrorsToRulesConverter converter = new FieldErrorsToRulesConverter(dbEntity.Table.TableName + "_FieldError", bizobj.MessageSource);
            foreach (IDbColumn field in dbEntity.Table.Columns)
                converter.AddField(field, captions.GetFieldCaption(field.PropertyName));

            return converter;
        }

        /// <summary>
        /// Validates BizElements' field costraints and transforms them to a collection of broken BusinessRule objects.
        /// </summary>        
        /// <param name="bizobj">Business object that is to be validated.</param>
        /// <param name="dbEntity">BizElements entity used by the given business object. Sometimes this will the same object.</param>
        /// <param name="captions">Objects that provides field names. Sometimes this will be the same object.</param>
        /// <param name="fieldErrorsConverter">Converts validation errors detected by BizElements framework into BusinessRule objects. SetCustomRuleDescription() method may be used to specify custom error/rule descriptions.</param>
        /// <returns>Collection of broken rules.</returns>
        public static BusinessRuleCollection CheckFieldConstraints(ISelfValidatingObject bizobj, IEntity dbEntity, ICaptionProvider captions, FieldErrorsToRulesConverter fieldErrorsConverter)
        {
            FieldError[] errors = dbEntity.ValidateAllFields();
            BusinessRuleCollection brokenBizElementsRules = fieldErrorsConverter.ConvertMulti(errors);
            return brokenBizElementsRules;
        }

        /// <summary>
        /// Validates BizElements' field costraints and transforms them to a collection of broken BusinessRule objects.
        /// </summary>
        /// <param name="bizobj">Business object that is to be validated.</param>
        /// <param name="dbEntity">BizElements entity used by the given business object. Sometimes this will the same object.</param>
        /// <param name="captions">Objects that provides field names. Sometimes this will be the same object.</param>
        /// <returns>Collection of broken rules.</returns>
        public static BusinessRuleCollection CheckFieldConstraints(ISelfValidatingObject bizobj, IEntity dbEntity, ICaptionProvider captions)
        {
            FieldError[] errors = dbEntity.ValidateAllFields();
            FieldErrorsToRulesConverter fieldErrorsConverter = CreateFieldErrorsToRulesConverter(bizobj, dbEntity, captions);
            BusinessRuleCollection brokenBizElementsRules = fieldErrorsConverter.ConvertMulti(errors);
            return brokenBizElementsRules;
        }        

        /// <summary>
        /// Specifies flags that control how the <b>CheckFieldConstraints</b> method is conducted.
        /// </summary>
        [Flags]
        public enum CheckFieldConstraintsFlags
        {
            /// <summary>Specifies no flags. Only validation implemented in BizElements OR/M framework is performed.</summary>
            Default = 0,
            /// <summary>Specifies that <b>NullError</b> is set for non-nullable FK fields which contain a value less than <b>1</b>.</summary>
            IntegerNoNullFkFieldsMustBeGreaterThanZero = 1,
            /// <summary>Specifies that <b>NullError</b> is set for non-nullable FK fields which are set to <b>zero</b>. Negative values are allowed.</summary>
            ZeroNotAllowedInIntegerFkField = 2
        }

        /// <summary>
        /// Creates a converter that transforms BizElements errors to BusinessRule objects.
        /// </summary>
        /// <param name="bizobj">Business object the converter is created for.</param>
        /// <returns>Business rule with a status that depends on BizElements error code.</returns>
        public static FieldErrorsToRulesConverter CreateFieldErrorsToRulesConverter<T>(T bizobj)
            where T : ISelfValidatingObject, IEntity, ICaptionProvider
        {
            FieldErrorsToRulesConverter converter = new FieldErrorsToRulesConverter(bizobj.Table.TableName + "_FieldError", ((ICaptionProvider)bizobj).MessageSource);
            foreach (IDbColumn field in bizobj.Table.Columns)
                converter.AddField(field, bizobj.GetFieldCaption(field.PropertyName));

            return converter;
        }
        

        /// <summary>
        /// Validates BizElements' field costraints and transforms them to a collection of broken BusinessRule objects.
        /// </summary>
        /// <param name="bizobj">Business object derived from BizElements generated entity that is to be validated.</param>
        /// <param name="options">Specifies flags that control how the operation is conducted.</param>
        /// <returns>Collection of broken rules.</returns>
        public static BusinessRuleCollection CheckFieldConstraints<T>(T bizobj, CheckFieldConstraintsFlags options)
            where T : ISelfValidatingObject, IEntity, ICaptionProvider
        {
            FieldErrorsToRulesConverter fieldErrorsConverter = CreateFieldErrorsToRulesConverter(bizobj, bizobj, bizobj);
            return CheckFieldConstraints<T>(bizobj, options, fieldErrorsConverter);
        }

        /// <summary>
        /// Validates BizElements' field costraints and transforms them to a collection of broken BusinessRule objects.
        /// </summary>
        /// <param name="bizobj">Business object derived from BizElements generated entity that is to be validated.</param>
        /// <param name="options">Specifies flags that control how the operation is conducted.</param>
        /// <param name="fieldErrorsConverter">Converts validation errors detected by BizElements framework into BusinessRule objects. SetCustomRuleDescription() method may be used to specify custom error/rule descriptions.</param>
        /// <returns>Collection of broken rules.</returns>
        public static BusinessRuleCollection CheckFieldConstraints<T>(T bizobj, CheckFieldConstraintsFlags options, FieldErrorsToRulesConverter fieldErrorsConverter)
            where T : ISelfValidatingObject, IEntity, ICaptionProvider
        {
            List<FieldError> errors = new List<FieldError>(bizobj.ValidateAllFields());

            bool integerNoNullFkFieldsMustBeGreaterThanZero = (options & CheckFieldConstraintsFlags.IntegerNoNullFkFieldsMustBeGreaterThanZero) > 0;
            if (integerNoNullFkFieldsMustBeGreaterThanZero && !HasErrors(errors))
                CheckIntegerNoNullFkFieldsMustBeGreaterThanZero<T>(bizobj, errors);

            bool zeroNotAllowedInIntegerFkField = (options & CheckFieldConstraintsFlags.ZeroNotAllowedInIntegerFkField) > 0;
            if (zeroNotAllowedInIntegerFkField && !HasErrors(errors))
                CheckZeroNotAllowedInIntegerFkField<T>(bizobj, errors);

            BusinessRuleCollection brokenBizElementsRules = fieldErrorsConverter.ConvertMulti(errors);
            return brokenBizElementsRules;
        }

        private static void CheckIntegerNoNullFkFieldsMustBeGreaterThanZero<T>(T bizobj, List<FieldError> errors) where T : ISelfValidatingObject, IEntity, ICaptionProvider
        {
            var intFkFields = ArrayUtil.FindAll<List<IDbColumn>, IDbColumn>(bizobj.Table.Columns, (col) => IsNonNullIntFkField(col));
            foreach (IDbColumn fkField in intFkFields)
            {
                object val = bizobj.GetField(fkField);
                bool hasValueAndLessThanOne = (val != null) && (Convert.ToInt64(val) < 1);
                if (hasValueAndLessThanOne)
                    errors.Add(new FieldError(fkField, FieldErrorCode.NullError, val));
            }
        }

        private static void CheckZeroNotAllowedInIntegerFkField<T>(T bizobj, List<FieldError> errors) where T : ISelfValidatingObject, IEntity, ICaptionProvider
        {
            var intFkFields = ArrayUtil.FindAll<List<IDbColumn>, IDbColumn>(bizobj.Table.Columns, (col) => IsNonNullIntFkField(col));
            foreach (IDbColumn fkField in intFkFields)
            {
                object val = bizobj.GetField(fkField);
                bool hasValueAndEqualsZero = (val != null) && (Convert.ToInt64(val) == 0);
                if (hasValueAndEqualsZero)
                    errors.Add(new FieldError(fkField, FieldErrorCode.NullError, val));
            }
        }

        private static bool IsNonNullIntFkField(IDbColumn col)
        {
            return col.IsForeignKeyPart && !col.IsNullable && TypeUtil.IsInteger(col.DataType);
        }

        private static bool HasErrors(IEnumerable<FieldError> errors)
        {
            return ArrayUtil.Exists<FieldError>(errors, (err) => (err.ErrorCode != FieldErrorCode.AllOk));
        }

        #endregion

        #region FetchCodeList.

        /// <summary>
        /// Fetches key-value pairs from the given fields' parent table.
        /// </summary>
        /// <param name="conn">Connection provider. May be NULL.</param>
        /// <param name="valueField">Value field. Usually a primary key field.</param>
        /// <param name="displayField">Display field. Usually a name that is visible in the UI.</param>
        /// <param name="filter">Filter. May be NULL.</param>
        /// <returns>CodeList.</returns>
        public static ICodeList FetchCodeList(IConnectionProvider conn, IDbColumn valueField, IDbColumn displayField, SearchCondition filter)
        {            
            IDbTable table = valueField.Table;
            if (table == null)
                throw new Exception("Parent table of the field must be set.");
            if (table.HasEqualAliasAndNameAs(displayField.Table) == false)
                throw new Exception("Value and display field must belong to the same table.");

            SelectStatement select = new SelectStatement(table);
            select.SelectList.Add(valueField);
            select.SelectList.Add(displayField);
            select.OrderBy.Add(displayField);

            if (filter != null)
                select.Where = filter;

            if (conn != null)
                select.ConnectionProvider = conn;

            DataTable data = select.Execute();
            ICodeList codeList = new CodeList(data, valueField.ColumnName, displayField.ColumnName);
            return codeList;
        }

        /// <summary>
        /// Fetches key-value pairs from the given fields' parent table.
        /// </summary>
        /// <param name="conn">Connection provider. May be NULL.</param>
        /// <param name="valueField">Value field. Usually a primary key field.</param>
        /// <param name="displayField">Display field. Usually a name that is visible in the UI.</param>
        /// <param name="filter">Filter. May be NULL.</param>
        /// <param name="orderByField">Field by which the data is sorted. If null, data is sorted by display field.</param>
        /// <returns>CodeList.</returns>
        public static ICodeList FetchCodeList(IConnectionProvider conn, IDbColumn valueField, IDbColumn displayField, SearchCondition filter, IDbColumn orderByField)
        {
            IDbTable table = valueField.Table;
            if (table == null)
                throw new Exception("Parent table of the field must be set.");
            if (table.HasEqualAliasAndNameAs(displayField.Table) == false)
                throw new Exception("Value and display field must belong to the sam table.");

            SelectStatement select = new SelectStatement(table);
            select.SelectList.Add(valueField);
            select.SelectList.Add(displayField);
            if (orderByField != null)
                select.OrderBy.Add(orderByField);
            else
                select.OrderBy.Add(displayField);

            if (filter != null)
                select.Where = filter;

            if (conn != null)
                select.ConnectionProvider = conn;

            DataTable data = select.Execute();
            ICodeList codeList = new CodeList(data, valueField.ColumnName, displayField.ColumnName);
            return codeList;
        }

        #endregion

        #region SetPropertyValue.

        /// <summary>
        /// Sets the given value into the business object's property.
        /// </summary>
        /// <param name="bizObject">Object which contains the specified property.</param>
        /// <param name="propertyName">Property which is to receive the given value.</param>
        /// <param name="val">Value compatibile with property's type.</param>
        /// <remarks><para>Objects which implement <b>BizElements.Core.IEntity</b> interface are guaranteed to except
        /// null values even if the specified public property is not nullable. This is possible because
        /// BizElements framework provides mechanism which inserts the values directly into class fields encapsulated
        /// by the generated public properties.</para>
        /// <para>For all other classes an atempt to insert a null value into a non-nullable property
        /// will result with an error.</para></remarks>
        public static void SetPropertyValue(object bizObject, string propertyName, object val)
        {
            bool isSet = false;
            if (bizObject is IEntity)
                isSet = SetEntityProperty((IEntity)bizObject, propertyName, val);

            if (!isSet)
            {
                PropertyInfo bizObjectProperty = bizObject.GetType().GetProperty(propertyName);
                if (bizObjectProperty == null)
                    throw new Exception("Specified property (" + propertyName + ") doesn't belong to class + " + bizObject.GetType().ToString() + ".");

                bizObjectProperty.SetValue(bizObject, val, null);
            }
        }

        /// <summary>
        /// Set entity's property.
        /// </summary>
        /// <param name="entity">Entity whose field is to be set.</param>
        /// <param name="propertyName">Property name of the field that is receiving the new value.</param>
        /// <param name="val">Value. Only .NET primitive types should be used.</param>
        /// <returns>True if the field exists; false if the field with the given property name doesn't exist.</returns>
        private static bool SetEntityProperty(IEntity entity, string propertyName, object val)
        {
            IDbColumn field = entity.Table.Columns.GetByPropertyName(propertyName);
            bool fieldExists = (field != null);

            if (field != null)
                entity.SetField(field, val);
            return fieldExists;
        }

        #endregion

        #region GetChangedPropertyNames.

        /// <summary>
        /// Compares all non-PK and non-autogenerated fields of two entities. Entities do not have to belong to the sames table.
        /// </summary>
        /// <param name="oldData">First entity.</param>
        /// <param name="newData">Second entity.</param>
        /// <returns>Array of property names that are not equal.</returns>
        public static string[] GetChangedPropertyNames(IEntity oldData, IEntity newData)
        {
            List<string> changedPropertyNames = new List<string>();
            foreach (IDbColumn oldColumn in oldData.Table.Columns)
            {
                bool isPkOrAutogen = oldColumn.IsPrimaryKeyPart || oldColumn.AutoIncrement || oldColumn.IsAutoGenerated;
                if (!isPkOrAutogen)
                {
                    var newColumn = newData.Table.Columns.GetByPropertyName(oldColumn.PropertyName);
                    if (newColumn != null)
                    {
                        string oldValue = Convert.ToString(oldData.GetField(oldColumn), CultureInfo.InvariantCulture);
                        string newValue = Convert.ToString(newData.GetField(newColumn), CultureInfo.InvariantCulture);
                        if (oldValue != newValue)
                            changedPropertyNames.Add(oldColumn.PropertyName);
                    }
                }
            }

            return changedPropertyNames.ToArray();
        }

        #endregion

        #region GetDefaultFieldsForAllParents.

        /// <summary>Gets default fields for all parents which implement <see cref="IDataObject"/> interface.</summary>
        /// <param name="bizObject">Entity whose parents are to be fetched.</param>
        /// <returns>Collection of columns to fetch.</returns>
        public static ICollection<IDbColumn> GetDefaultFieldsForAllParents(IBusinessObject bizObject)
        {
            List<IDbColumn> allDefaultFields = new List<IDbColumn>();
            foreach (DbRelation fk in bizObject.Table.ForeignKeys)
            {
                IDbColumn defaultField = TryGetDefaultField(fk.Parent);
                if (defaultField != null)
                    allDefaultFields.Add(defaultField);
            }

            return allDefaultFields;
        }

        private static IDbColumn TryGetDefaultField(IDbTable parentTable)
        {
            IDbColumn defaultField;
            IDataObject parent = parentTable.NewEntity() as IDataObject;
            if (parent != null)
            {
                string defaultPropertyName = parent.GetDataObjectDescriptor().Fields.DefaultPropertyName;
                defaultField = (defaultPropertyName != null) ? parent.Table.Columns.GetByPropertyName(defaultPropertyName) : null;
            }
            else
            {
                defaultField = null;
            }

            return defaultField;
        }

        #endregion
    }    
}